require 'uri'

class Term < ActiveRecord::Base
  has_many :courses, :dependent=>:destroy
  validates_uniqueness_of :name,:start_date,:end_date
  
  attr_accessor :verboseParsing

  # Process query with the latest term
  def query(fields, offset=0, limit=25)
    fields = fields.dup
    fields[:term_id] = self.id

    # If time range is specified, canonicalize the date to 1/1/2000 to avoid comparing dates
    if fields[:start_time]
      startTime = fields[:start_time]
      startTime = Time.mktime(2000,1,1,startTime.hour,startTime.min)
      stopTime = fields[:stop_time]
      stopTime = Time.mktime(2000,1,1,stopTime.hour,stopTime.min)
      fields[:start_time] = startTime..stopTime
      fields[:stop_time] = startTime..stopTime
    end

    # While Term.query supports finding events other than lecture,
    # search controller does not send a query for DIS or LAB (1/16/2010).
    fields[:event_type] = 'LEC' if not fields[:event_type]
    fields[:location].upcase! if fields[:location]

    # term_id, course_number, and department are fields of a course
    fields = query_push_down_field(fields, :courses, :term_id)
    fields[:course_number].upcase! if fields[:course_number]
    fields = query_push_down_field(fields, :courses, :course_number)
    fields = query_push_down_field(fields, :courses, :department)

    # last_name is a field of instructor
    fields[:last_name].upcase! if fields[:last_name]
    fields = query_push_down_field(fields, :instructors, :last_name)

    # Join instructos only when last_name is specified
    # Course is always specified because term_id is always present
    if fields[:instructors]
      events = Lecture.find(:all,
                          :conditions => fields,
                          :joins => {:course => {}, :instructors => {}},
                          :offset => offset,
                          :limit => limit)
    else
      events = Event.find(:all,
                          :conditions => fields,
                          :joins => {'course' => {}},
                          :offset => offset,
                          :limit => limit)
    end

    return events
  end

  # Move field value inside fields[:course]
  private
  def query_push_down_field(fields, to, key)
    if fields[key]:
      fields[to] ||= {}
      fields[to][key] = fields[key]
      fields.delete key
    end
    return fields
  end

  public

  def Term.fetch_departments
    begin
      fh = open('http://schedule.berkeley.edu/srchfall.html')
    rescue
    end
    doc = Hpricot(fh)
    list = (doc/"select[@name='p_deptname']/option").collect { |opt| opt.innerText.strip }
    return list.select {|name| not name.match(/Choose.+Department/i)}
  end

  # Grab schedule data for terms from schedule.berkeley.edu for the specified uri
  # And returns the next uri if there is any
  # The return value of this method indicates success or error occures
  # It's enum of the following symbols:
  #   :success - method successfully fetched the page and synced the contents with the database
  #   :failed_fetch - method failed to open the remote file
  #   :no_results - the specified page didn't contain any events (empty result)
  #   :term_mismatch - the specified page contained events for a different term than self
  #                    this error is caused most likely by Berkeley placing schedule for new term
  def fetch_page_and_sync(uri)
    begin
      fh = open(uri)
    rescue
      return {:status => :failed_fetch} if not fh
    end

    doc = Hpricot(fh)
    next_uri = nil
    is_correct_term = false

    # For each table element, parse the inner text
    (doc/:table).each do |event|
      inner_text = event.inner_text

      # If department name doesn't match, return no_results
      if inner_text.match(/No\s+departments\s+match\s+your\s+request/i):
        return {:status => :no_results}

      # If "matches to your request for <term name>" appears, then it's the correct term
      # so go ahead and parse the results. If this never appears, we're parsing a wrong page,
      # so ignore everything and error with term_mismatch later
      elsif inner_text.match(/matches\s+to\s+your\s+request\s+for\s+#{@name}/i):
        is_correct_term = true

      # If text "see next results" appears, then look for the anchor element around it
      # and obtain the uri for the next page
      elsif inner_text.match(/see\s+next\s+results/i):
        (event/"a").each { |atag| next_uri = atag.attributes['href'] if atag.inner_text.match(/see\s+next\s+results/i) }

      elsif is_correct_term
#        p '[[[['
#        p event.innerText
#        p ']]]]'
#        p
        parsed = Term.parse_class_block(inner_text)
        p parsed
        create_objects_for_parsed_event(parsed[:course], parsed[:event], parsed[:instructors]) if parsed
#        syncParseEventAndAdd(event.inner_text)
      end
    end

    return {:status => :term_mismatch} if not is_correct_term

    return {:status => :success, :uri => next_uri}
  end

  def syncParseMsg(msg)
    @debug ||= ''
    @debug += msg
    @debug += "<br/>" if msg != '.' and not (msg =~ /\</)
    return @debug
  end

  # This method parses a block of texts that represents each lecture, discussion, etc...
  # The block is extracted as the inner text of table element
  # It returns three hashes course, event, and instructors as a single hash
  # where each hash is accessed by the same key
  def Term.parse_class_block(text)
    course = {}
    event = {:cancelled => false, :see_dept => false,
      :sunday => false, :monday => false, :tuesday => false, :wednesday => false,
      :thursday => false, :friday => false, :saturday => false}
    inst = []

    text.each do |line|
      # skip empty line; a slight optimization
      line = line.gsub(/[^\x20-\x7e]/,' ')
      next if line.match(/^[ \t]*$/)

      # Extract <course title> from "Course Title: <course title> (catalog description)"
      if m = line.match(/course\s+title\s*\:\s*([A-Za-z].+[^\s])\s*(\(catalog\s+description\))/i)
        course[:title] = m[1]
 
      # Extract course control number from "Course Control Number: <ccn> View Books"
      elsif m = line.match(/course\s+control\s+number\s*\:\s*(\d+)/i)
        event[:ccn] = m[1].to_i

      # Extract course control number from "Course Control Number: <ccn> View Books"
      elsif m = line.match(/course\s+control\s+number\s*\:\s*SEE\s+DEPT/i)
       event[:see_dept] = true

      # Extract department, course number, section number, and event type
      # from "Course: <department> <course number> P or S <section number> <event type>"
      elsif m = line.match(/course\s*:\s*(([A-Za-z\/\,\s]+)\s+(\w{0,2}\d+\w*)\s+([PS])\s+(\d{3,3}|\d+\-\d+)\s+([A-Z]{3,3}))/i)
        course[:department] = m[2].upcase
        course[:course_number] = m[3]
        event[:is_primary] = m[4].upcase == 'P' ? true : false
        event[:section_number] = m[5]
        event[:event_type] = m[6].upcase

      # Extract location and time from "Location: <days of week> <time>, <location>
      elsif m = line.match(/location\s*:\s*(UNSCHED|(((Su)?(M)?(Tu)?(W)?(Th)?(F)?(Sa)?(Su)?)\s*(\d+\-\d+[PA])))\s*,?\s*([^(]+[^\s(])/i)
        if not m[1].match(/^UNSCHED$/i)
          event[:sunday] = (m[4] or m[11]) ? true : false
          event[:monday] = m[5] ? true : false
          event[:tuesday] = m[6] ? true : false
          event[:wednesday] = m[7] ? true : false
          event[:thursday] = m[8] ? true : false
          event[:friday] = m[9] ? true : false
          event[:saturday] = m[10] ? true : false
          time = self.parse_class_time(m[12])
          event[:start_time] = time[:start_time]
          event[:stop_time] = time[:stop_time]
        end
        event[:location] = m[13]

      # Extract Units from "Units/Credit: <units>"
      elsif m = line.match(/Units\s*\/\s*Credit\s*:\s*(\d+|(\d*\.\d+))(\-(\d+|(\d*\.\d+)))?\:?\s+.*/i)
        if m[4] and m[1].to_f >= 0 and m[4].to_f >= 0 and m[1].to_f < m[4].to_f
          course[:units_range] = [m[1].to_f, m[4].to_f]
        else
          course[:units] = m[1].to_f
        end

      # Extract finals group from "Final Exam Group: <finals group>"
      elsif m = line.match(/Final\s+Exam\s+Group\s*\:\s*[^A-Za-z]?\s*(\d+).*/i)
        event[:finals_group] = m[1].to_i

      # Extract the first insturctor's name from  "Instructor: <last name>, <first name>"
      elsif m = line.match(/Instructor\s*:\s*(\w+),\s*(\w(\s\w|\w)*).*/i)
        inst.push({:department => course[:department], :last_name => m[1], :first_name => m[2]})

      # Notice if the event was cancelled
      elsif m = line.match(/[^:]+\:[^C]+CANCELLED/i)
        event[:cancelled] = true

      # Extract subtitle and other instructors' names
      elsif m = line.match(/Note\s*:\s*(.+)/i)
        note = m[1]
        # If double quotations appear, that's a subtitle
        # subtitle is bound for each event rather than course
        if m = note.match(/^\"(.+)\"/i)
          event[:subtitle] = m[1]
        end

        # If "Also:" appears, then the following words will be instructor's name
        if m = note.match(/Also\s*:\s*(\w+),\s*(\w(\s\w|\w)*).*/i)
          inst.push({:department => course[:department], :last_name => m[1], :first_name => m[2]})
        end
      end
    end

    return nil if not course[:department] or not course[:course_number] or not event[:event_type]

    event[:event_name] = course[:department].to_s+" "+course[:course_number].to_s+
      " "+event[:event_type].to_s+" "+event[:section_number].to_s

    return {:course => course, :event => event, :instructors => inst}
  end

  def Term.parse_class_time(str)
    s = str.match(/((\d{1,2})(30)?)\s*-\s*((\d{1,2})(30)?)([AP])/i)
    return null if not s
    start30 = s[3]
    start30 = '0' if not start30
    end30 = s[6]
    end30 = '0' if not end30
    ampm = "AM" if s[7].upcase == 'A'
    ampm = "PM" if s[7].upcase == 'P'
    stop = Time.parse(s[5]+":"+end30+ampm)
    start = Time.parse(s[2]+":"+start30+"pm")
    start = Time.parse(s[2]+":"+start30+"am") if start > stop
    return {:start_time => Time.utc(2000,1,1,start.hour,start.min),
      :stop_time => Time.utc(2000,1,1,stop.hour,stop.min)}
  end

  def create_objects_for_parsed_event(course, event, instructors)
    if event[:see_dept] or event[:event_type] == 'SLF' or course[:units_range] # temporary resolution
      return {:course => nil, :event => nil, :instructors => nil}
    end

    course[:term_id] = self.id
    c = Course.update_course(course)
    event.delete(:see_dept)
    event.delete(:cancelled)
    event[:course_id] = c.id

    # Treat seminar as lecture
    event[:event_type] = 'LEC' if event[:event_type] == 'SEM'

    begin
      e = Lecture.create!(event) if event[:event_type] == 'LEC'
      e = Discussion.create!(event) if event[:event_type] == 'DIS'
      e = Lab.create!(event) if event[:event_type] == 'LAB'
    rescue
      e = nil
    end

    is = nil
    if e and e.is_primary?
      instructors.each do |inst|
        i = Instructor.add_unless_dup(inst)
        e.instructors << i
      end
      is = e.instructors
    end

    return {:course => c, :event => e, :instructors => is}
  end

  def syncParseEventAndAdd(text)
    courseInfo = {}
    eventInfo = {}
    instInfo = {}
    altInstInfo = {}

    text.each do |line|
      # skip empty line; a slight optimization
      t = ''
      p line
      line = line.gsub(/[^\x20-\x7e]/,' ')
      next if line.match(/^[ \t]*$/)
      if m = line.match(/course\s+title\s*\:\s*([A-Za-z][^(]+[^( \s])\s*(\(.+\))?/i)
        courseInfo[:title] = m[1]
      elsif m = line.match(/course\s+control\s+number\s*\:\s*(\d+)/i)
        eventInfo[:ccn] = m[1]
      elsif m = line.match(/course\s*:\s*(([A-Za-z\/\,\s]+)\s+(\w{0,2}\d+\w*)(\s+[PS])?\s+(\d{3,3})\s+(LEC|LAB|DIS|SLF|SEM|STD|IND|COL|GRP|REC))/i)
        courseInfo[:department] = m[2].upcase
        courseInfo[:course_number] = m[3]
        eventInfo[:section_number] = m[5]
        eventInfo[:event_type] = m[6].upcase
      elsif m = line.match(/location\s*:\s*((Su)?(M)?(Tu)?(W)?(Th)?(F)?(Sa)?(Su)?)\s*(\d+\-\d+[PA])\s*,?\s*(.+\S)/i)
        #eventInfo[:day] = m[1]
        eventInfo[:sunday] = (m[2] or m[9]) ? true : false
        eventInfo[:monday] = m[3] ? true : false
        eventInfo[:tuesday] = m[4] ? true : false
        eventInfo[:wednesday] = m[5] ? true : false
        eventInfo[:thursday] = m[6] ? true : false
        eventInfo[:friday] = m[7] ? true : false
        eventInfo[:saturday] = m[8] ? true : false
        time = syncParseTime(m[10])
        eventInfo[:start_time] = time[0]
        eventInfo[:stop_time] = time[1]
        eventInfo[:location] = m[11]
      elsif m = line.match(/Units\s*\/\s*Credit\s*:\s*[^A-Za-z]?\s*(\d+):?.*/i)
        courseInfo[:units] ||= m[1]
      elsif m = line.match(/Final\s+Exam\s+Group\s*\:\s*[^A-Za-z]?\s*(\d+).*/i)
        eventInfo[:finals_group] = m[1]
      elsif m = line.match(/Instructor\s*:\s*(\w+),\s*(\w(\s\w|\w)*).*/i)
        instInfo[:last_name] = m[1]
        instInfo[:first_name] = m[2]
      elsif m = line.match(/[^:]+\:[^C]+CANCELLED/i)
        eventInfo[:cancelled] = true
      elsif m = line.match(/Note\s*:\s*(.+)/i)
        note = m[1]
        if m = note.match(/^\"([\w ]+)\"/i)
          courseInfo[:subtitle] = m[1]
        end
        if m = note.match(/Also\s*:\s*(\w+),\s*(\w(\s\w|\w)*).*/i)
          altInstInfo[:last_name] = m[1]
          altInstInfo[:first_name] = m[2]
        end
      end
    end

    # Skip self-paced and directed-group study (type is enum of LEC|LAB|DIS|SLF|SEM|GRP)
    return if eventInfo[:event_type] == 'GRP' or eventInfo[:event_type] == 'SLF'
    # Also skip any event that does not specify ccn or starting / ending time
    return if not eventInfo[:start_time] or not eventInfo[:stop_time] or not eventInfo[:ccn]

    eventInfo[:event_type] = 'LEC' if eventInfo[:event_type] == 'SEM'
    eventInfo[:event_type] = 'LEC' if eventInfo[:event_type] == 'STD'
    eventInfo[:event_type] = 'LEC' if eventInfo[:event_type] == 'IND'
    eventInfo[:event_type] = 'DIS' if eventInfo[:event_type] == 'REC'
    
    p eventInfo.inspect
    p courseInfo.inspect

    courseInfo.delete(:subtitle)
    courseInfo[:term_id] = id
    courses = Course.find(:all, :conditions =>
                { :department => courseInfo[:department],
                  :course_number => courseInfo[:course_number],
                  :term_id => id })
    # If there is no entry, add the course
    if courses.length == 0:
      course = Course.create!(courseInfo)
      syncParseMsg("Failed to create a course: "+instInfo[:department]+" "+instInfo[:course_number]) if not course
    else
      course = Course.update(courses[0].id, courseInfo)
    end

    eventInfo[:event_name] = courseInfo[:department].to_s+" "+courseInfo[:course_number].to_s+
      " "+eventInfo[:event_type].to_s+" "+eventInfo[:section_number].to_s
    events = Event.find(:all, :conditions => {:ccn => eventInfo[:ccn]})
    eventInfo[:course] = course
    if events.length == 0
      event = Lecture.create!(eventInfo) if eventInfo[:event_type] == 'LEC'
      event = Discussion.create!(eventInfo) if eventInfo[:event_type] == 'DIS'
      event = Lab.create!(eventInfo) if eventInfo[:event_type] == 'LAB'
    else
      event = Event.update(events[0].id, eventInfo)
    end

    if not event
      syncParseMsg("Failed to instantiate:"+eventInfo[:event_name])
      return
    end

    if event.event_type == 'LEC' and event.instructors.length == 0
      # Make sure instructors are added
      inst = syncAddInstructor(instInfo) if instInfo[:last_name]
      altInst = syncAddInstructor(altInstInfo) if altInstInfo[:last_name]

      if inst
        event.instructors << inst if inst
        teaching = Teaching.find(:first,:conditions=>{:instructor_id=>inst,:event_id=>event})
        syncFillValuesForTeaching(teaching)
      end
      if altInst
        event.instructors << altInst if altInst
        teaching = Teaching.find(:first,:conditions=>{:instructor_id=>altInst,:event_id=>event})
        syncFillValuesForTeaching(teaching)
      end
    end

    if verboseParsing
      syncParseMsg("<h3>EVENT:"+eventInfo[:event_name]+"</h3>")
      if (eventInfo[:cancelled])
        syncParseMsg("This class is cancelled")
      else
        syncParseMsg("course-department:"+course.department) if course.department
        syncParseMsg("course-number:"+course.course_number) if course.course_number
        syncParseMsg("course-units:"+course.units.to_s) if course.units
        syncParseMsg("event-section:"+event.section_number) if event.section_number
        syncParseMsg("event-type:"+event.event_type) if event.event_type
        syncParseMsg("event-monday:"+event.monday.to_s) if event.monday
        syncParseMsg("event-tuesday:"+event.tuesday.to_s) if event.tuesday
        syncParseMsg("event-wednesday:"+event.wednesday.to_s) if event.wednesday
        syncParseMsg("event-thursday:"+event.thursday.to_s) if event.thursday
        syncParseMsg("event-friday:"+event.friday.to_s) if event.friday
        syncParseMsg("event-saturday:"+event.saturday.to_s) if event.saturday
        syncParseMsg("event-sunday:"+event.sunday.to_s) if event.sunday
        syncParseMsg("event-start-time:"+event.start_time.to_s) if event.start_time
        syncParseMsg("event-stop-time:"+event.stop_time.to_s) if event.stop_time
        syncParseMsg("event-location:"+event.location) if event.location
        syncParseMsg("event-finals:"+event.finals_group.to_s) if event.finals_group
        syncParseMsg("event-ccn:"+event.ccn.to_s) if event.ccn
        if inst
          syncParseMsg("inst1-first:"+inst.first_name) if inst.first_name
          syncParseMsg("inst1-last:"+inst.last_name) if inst.last_name
        end
        if altInst
          syncParseMsg("inst2-first:"+altInst.first_name) if altInst.first_name
          syncParseMsg("inst2-last:"+altInst.last_name) if altInst.last_name
        end
      end
    end

    syncParseMsg(".")
  end

  def syncParseTime(time)
    s = time.match(/((\d{1,2})(30)?)\s*-\s*((\d{1,2})(30)?)([AP])/i)
    return null if not s
    start30 = s[3]
    start30 = '0' if not start30
    end30 = s[6]
    end30 = '0' if not end30
    ampm = "AM" if s[7].upcase == 'A'
    ampm = "PM" if s[7].upcase == 'P'
    endTime = Time.parse(s[5]+":"+end30+ampm)
    startTime = Time.parse(s[2]+":"+start30+"pm")
    if startTime > endTime
      startTime = Time.parse(s[2]+":"+start30+"am")
    end
    endTime = Time.mktime(2000,1,1,endTime.hour,endTime.min)
    startTime = Time.mktime(2000,1,1,startTime.hour,startTime.min)
    return [startTime, endTime]
  end

  def syncAddInstructor(instInfo)
    insts = Instructor.find(:all, :conditions => { :last_name => instInfo[:last_name] })
    syncParseMsg("Conflicting instructor's last name: " + instInfo[:last_name]) if insts.length > 1
    if insts.length == 0:
      inst = Instructor.create!(instInfo)
      syncParseMsg("Failed to create an entry for "+instInfo[:last_name]+" "+instInfo[:fisrt_name]) if not inst
    else
      inst = insts[0]
    end
    return inst
  end
  
  def syncFillValuesForTeaching(teaching)
    # temporarily disable HKN rating feature because the script on hkn side is missing now
    return

    prof = teaching.instructor
    lecture = teaching.lecture
    course = lecture.course
    dept = nil
    if lecture.course.department.casecmp("CS") or lecture.course.department.casecmp("computer science")
      dept = "CS"
    elsif lecture.course.department.casecmp("EE") or lecture.course.department.casecmp("electrical engineering")
      dept = "EE"
    else
      return
    end

    uri = URI.escape("http://hkn.eecs.berkeley.edu/~conorh/get_ratings.cgi?lastname=#{prof.last_name}&firstname=#{prof.first_name}&department=#{dept}&number=#{course.course_number}")
    output = open(uri)
    success = output.gets.chomp
    return if success != "success"
    instructorName = output.gets.chomp
    courseName = output.gets.chomp
    effectivenessStr = output.gets.chomp
    worthwhilenessStr = output.gets.chomp
    if effectivenessStr.match(/^[\d]+(\.[\d]+){0,1}$/) and worthwhilenessStr.match(/^[\d]+(\.[\d]+){0,1}$/)
      teaching.effectiveness = effectivenessStr.to_f
      teaching.usefulness    = worthwhilenessStr.to_f
      teaching.save!
    end
  end

end
